package owg.util.editor2d;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.border.TitledBorder;

import owg.util.InstanceCondition;
import owg.util.awt.StringFormattedEditor;
import owg.util.awt.TypeEditor;
import owg.util.data.property.Property;
import owg.util.data.property.PropertyImmutable;
import owg.util.data.property.PropertyListener;
import owg.util.data.property.PropertyUtil;
import owg.util.editor2d.Editor2D.Pivot;
import owg.util.editor2d.entity.Placeable2D;
import owg.util.editor2d.entity.Rotateable2D;
import owg.util.editor2d.entity.SP;
import owg.util.editor2d.entity.Scaleable2D;
import owg.util.editor2d.operations.Operation;
import owg.util.editor2d.operations.Transformation;

public class ObjectParamsPane extends JPanel
{
	private static final long serialVersionUID = -990822782877871532L;	
	public final Editor2DUI editorUI;
	private TitledBorder border;
	
	private boolean locked = false;
	private Placeable2D currentPivot = null;
	
	final PropertyListener<Pivot> pivotListener;
	private interface Disposable
	{
		public void updateValues();
		public void setEnabledBulk(boolean b);
	}
	private class ObjEditors implements Disposable
	{	
		private final JScrollPane scrollpane;
		
		private final Property<?>[] properties;
		private final JLabel[] propertyLabels;
		private final TypeEditor<?>[] propertyEditors;
		private final JLabel nameLabel;
		private final TypeEditor<String> nameEditor;
		private final JLabel xLabel, yLabel;
		private final TypeEditor<Double> xEditor,yEditor;
		private final JLabel xScaleLabel, yScaleLabel;
		private final TypeEditor<Float> xScaleEditor,yScaleEditor;
		private final JLabel angleLabel;
		private final TypeEditor<Float> angleEditor;
		
		
		public ObjEditors(JPanel target)
		{
			ActionListener stdParamListener = new ActionListener()
			{
				
				@Override
				public void actionPerformed(ActionEvent e)
				{
					editorUI.editor.enqueue(new Runnable()
					{
						
						@Override
						public void run()
						{
							final InstanceCondition cond;
							if(e.getSource() == xScaleEditor || e.getSource() == yScaleEditor)
								cond = (o)->{return o instanceof Scaleable2D;};
							else if(e.getSource() == angleEditor)
								cond = (o)->{return o instanceof Rotateable2D;};
							else if(e.getSource() == xEditor || e.getSource() == yEditor)
								cond = (o)->{return true;};
							else
								cond = (o)->{return o == currentPivot;};
								
									
							Integer[] selected = getAffectedIndicesFor(cond);
							Operation inv = new Transformation(editorUI.editor, selected);//Store everything before operation
							
							final String message;
							//Do stuff
							if(e.getSource() == xScaleEditor || e.getSource() == yScaleEditor)
								message = "Scale selected objects";
							else if(e.getSource() == angleEditor)
								message = "Rotate selected objects";
							else if(e.getSource() == xEditor || e.getSource() == yEditor)
								message = "Relocate selected objects";
							else if(e.getSource() == nameEditor)
								message = "Change name of pivot object";
							else
								message = "Unknown operation: "+e.getSource();
							for(int index : selected)
							{
								Placeable2D p = editorUI.editor.worldObjectsImmutable.get(index).a;
								
								if(e.getSource() == xScaleEditor || e.getSource() == yScaleEditor)
									((Scaleable2D)p).setScale(xScaleEditor.getValue(), yScaleEditor.getValue());
								else if(e.getSource() == angleEditor)
									((Rotateable2D)p).setAngle(angleEditor.getValue());
								else if(e.getSource() == xEditor || e.getSource() == yEditor)
									p.setLocation(xEditor.getValue(), yEditor.getValue());
								else if(e.getSource() == nameEditor)
									p.setName(nameEditor.getValue());
							}
							Operation op = new Transformation(editorUI.editor, selected);//Store everything after operation
							editorUI.editor.execute(op, inv, true, message, true, false);
							
						}
					});
				}
			};
			
			JPanel p = new JPanel();
			p.setLayout(new GridBagLayout());
			GridBagConstraints c = new GridBagConstraints();
			c.gridx = 0;
			c.gridy = 0;
			c.fill = GridBagConstraints.HORIZONTAL;
			c.weighty = 0;
			
			c.gridx = 0;
			c.gridwidth = 1;
			c.weightx = 0;
			p.add(nameLabel = new JLabel("Name: "),c);
			c.gridx = 1;
			c.gridwidth = 3;
			c.weightx = 1;
			nameEditor = new StringFormattedEditor<>(currentPivot.getName());
			nameEditor.addActionListener(stdParamListener);
			p.add(nameEditor, c);
			c.gridy++;
			
			c.gridx = 0;
			c.gridwidth = 1;
			c.weightx = 0;
			p.add(xLabel = new JLabel("X: "),c);
			c.gridx = 1;
			c.gridwidth = 1;
			c.weightx = 1;
			xEditor = new StringFormattedEditor<>(currentPivot.getX());
			xEditor.addActionListener(stdParamListener);
			p.add(xEditor, c);
			
			c.gridx = 2;
			c.gridwidth = 1;
			c.weightx = 0;
			p.add(yLabel = new JLabel("Y: "),c);
			c.gridx = 3;
			c.gridwidth = 1;
			c.weightx = 1;
			yEditor = new StringFormattedEditor<>(currentPivot.getY());
			yEditor.addActionListener(stdParamListener);
			p.add(yEditor, c);
			c.gridy++;
			
			if(currentPivot instanceof Rotateable2D)
			{
				c.gridx = 0;
				c.gridwidth = 1;
				c.weightx = 0;
				p.add(angleLabel = new JLabel("Angle: "),c);
				c.gridx = 1;
				c.gridwidth = 3;
				c.weightx = 1;
				angleEditor = new StringFormattedEditor<>(((Rotateable2D) currentPivot).getAngle());
				angleEditor.addActionListener(stdParamListener);
				p.add(angleEditor, c);
				c.gridy++;
			}
			else
			{
				angleLabel = null;
				angleEditor = null;
			}
			
			if(currentPivot instanceof Scaleable2D)
			{
				c.gridx = 0;
				c.gridwidth = 1;
				c.weightx = 0;
				p.add(xScaleLabel = new JLabel("xScale: "),c);
				c.gridx = 1;
				c.gridwidth = 1;
				c.weightx = 1;
				xScaleEditor = new StringFormattedEditor<>(((Scaleable2D) currentPivot).getXScale());
				xScaleEditor.addActionListener(stdParamListener);
				p.add(xScaleEditor, c);
				
				c.gridx = 2;
				c.gridwidth = 1;
				c.weightx = 0;
				p.add(yScaleLabel = new JLabel("yScale: "),c);
				c.gridx = 3;
				c.gridwidth = 1;
				c.weightx = 1;
				yScaleEditor = new StringFormattedEditor<>(((Scaleable2D) currentPivot).getYScale());
				yScaleEditor.addActionListener(stdParamListener);
				p.add(yScaleEditor, c);
				c.gridy++;
			}
			else
			{
				xScaleEditor = null;
				yScaleEditor = null;
				xScaleLabel = null;
				yScaleLabel = null;
			}
			
			
			c.gridx = 0;
			c.gridwidth = 4;
			c.weightx = 0;
			c.fill = GridBagConstraints.BOTH;
			
			properties = PropertyUtil.getProperties(currentPivot);
			propertyEditors = new TypeEditor[properties.length];
			propertyLabels = new JLabel[properties.length];
			
			JSeparator sep;
			sep = new JSeparator();
			sep.setPreferredSize(new Dimension(8,8));
			p.add(sep, c);
			c.gridy++;
			p.add(new JLabel(properties.length+" Custom "+(properties.length==1?"Property":"Properties")), c);
			c.gridy++;
			sep = new JSeparator();
			sep.setPreferredSize(new Dimension(8,8));
			p.add(sep, c);
			c.gridy++;
			
			boolean anyStretch = false;
			
			c.fill = GridBagConstraints.HORIZONTAL;
			for (int i = 0; i < properties.length; i++)
			{
				Property<?> prop = properties[i];
				final String name = PropertyUtil.nameOf(prop, currentPivot);
				boolean stretch = ArrayList.class.isAssignableFrom(prop.getClass()) || prop.getClass().isArray();
				if(stretch)
				{
					c.weighty = 1;
					anyStretch = true;
				}
				else
					c.weighty = 0;
				c.gridx = 0;
				c.gridwidth = 1;
				c.weightx = 0;
				p.add(propertyLabels[i] = new JLabel(prop.getType().getSimpleName()+" "+name) ,c);
				c.gridx = 1;
				c.gridwidth = 3;
				c.weightx = 1;
				final int thisPropertyIndex = i;
				propertyEditors[i] = getEditor(prop);
				propertyEditors[i].addActionListener(new ActionListener()
				{
					@Override
					public void actionPerformed(ActionEvent e)
					{
						editorUI.editor.enqueue(new Runnable()
						{
							
							@Override
							public void run()
							{
								Object val = propertyEditors[thisPropertyIndex].getValue();
								//Affect only instances of the pivot object class
								Integer[] selected = getAffectedIndicesFor((o) -> {return (currentPivot.getClass().isInstance(o));});
								
								Operation inv = new Transformation(editorUI.editor, selected);//Store everything before operation
								Operation op = new Transformation(editorUI.editor, selected, name, val);//Store everything after operation
								editorUI.editor.execute(op, inv, true, "Changed "+name+" to "+val+
									"on "+selected.length+" objects", true, false);
								
							}
						});
					}
				});
				p.add(propertyEditors[i], c);
				c.gridy++;
			}
			if(!anyStretch)
			{
				c.gridx = 0;
				c.gridwidth = 1;
				c.weightx = 0;
				c.weighty = 1;
				Dimension min = new Dimension(0,0);
				Dimension max = new Dimension(Integer.MAX_VALUE,Integer.MAX_VALUE);
				Box.Filler filler = new Box.Filler(min, min, max);
				p.add(filler,c);
				c.gridy++;
			}
			
			scrollpane = new JScrollPane(p);
			target.add(scrollpane);
		}
		
		public void updateValues()
		{
			nameEditor.setValue(currentPivot.getName());
			xEditor.setValue(currentPivot.getX());
			yEditor.setValue(currentPivot.getY());
			if(currentPivot instanceof Scaleable2D)
			{
				xScaleEditor.setValue(((Scaleable2D) currentPivot).getXScale());
				yScaleEditor.setValue(((Scaleable2D) currentPivot).getYScale());
			}
			if(currentPivot instanceof Rotateable2D)
				angleEditor.setValue(((Rotateable2D) currentPivot).getAngle());
			for (int i = 0; i < propertyEditors.length; i++)
				set(propertyEditors[i], properties[i].get());
		}
		private<X> void set(TypeEditor<X> typeEditor, Object object)
		{
			typeEditor.setValue(typeEditor.cast(object));
		}

		public void setEnabledBulk(boolean b)
		{
			nameEditor.setEnabled(b);
			xEditor.setEnabled(b);
			yEditor.setEnabled(b);
			if(xScaleEditor != null)
			{
				xScaleEditor.setEnabled(b);
				yScaleEditor.setEnabled(b);
				xScaleLabel.setEnabled(b);
				yScaleLabel.setEnabled(b);
			}
			if(angleEditor != null)
			{
				angleEditor.setEnabled(b);
				angleLabel.setEnabled(b);
			}
			
			nameLabel.setEnabled(b);
			xLabel.setEnabled(b);
			yLabel.setEnabled(b);
			for (int i = 0; i < propertyEditors.length; i++)
			{
				propertyLabels[i].setEnabled(b);
				propertyEditors[i].setEnabled(b);
			}
		}
	}

	private Disposable disposable = null;
	
	public ObjectParamsPane(Editor2DUI ui)
	{
		this.editorUI = ui;
		setLayout(new BorderLayout());
		currentPivot = ui.editor.getPivotObject();
		border = BorderFactory.createTitledBorder("");
		rebuild();
		
		setBorder(border);
		
		pivotListener = new PropertyListener<Editor2D.Pivot>()
		{
			@Override
			public void valueChanged(PropertyImmutable<? extends Pivot> property, 
				Pivot oldValue, Pivot newValue)
			{
				maybePivotChanged();
			}
		};
	}
	/**Called when {@link Editor2D.Pivot} mode changes and when the selected objects have changed because of an {@link Operation}.*/
	public void maybePivotChanged()
	{
		Placeable2D newPivot = editorUI.editor.getPivotObject();
		if(newPivot != currentPivot)
		{
			currentPivot = newPivot;
			rebuild();
		}
	}

	private void rebuild()
	{
		disposable = null;
		removeAll();
		if(currentPivot == null)
		{
			border.setTitle("Instance editor: [No Object]");
		}
		else
		{
			border.setTitle("Instance editor: "+currentPivot.getClass().getName()+"(ID="+currentPivot.getID()+")");
			disposable = new ObjEditors(this);
			disposable.setEnabledBulk(!locked);
		}
		invalidate();
		repaint();
	}

	private<X> TypeEditor<X> getEditor(Property<X> prop)
	{
		return TypeEditorFactory.getEditor(prop.getType(), prop.get(), new SP(PropertyUtil.getAnnotations(prop, currentPivot)));
	}
	public void setLocked(boolean b)
	{
		locked = b;
		if(disposable != null)
			disposable.setEnabledBulk(!b);
	}
	public void maybePivotParamsChanged()
	{
		if(disposable != null)
			disposable.updateValues();
	}
		
	/**Return a list of indices in the worldObjects list which will be affected.
	 * Only selected objects will be included, and only objects satisfying the condition will be included.*/
	private Integer[] getAffectedIndicesFor(InstanceCondition cond)
	{
		ArrayList<Integer> list = new ArrayList<>();
		for(Integer index : editorUI.editor.selectedIndices)
		{
			cond.accept(editorUI.editor.worldObjectsImmutable.get(index).a);
				list.add(index);
		}
		Integer[] selected = new Integer[list.size()];
		return list.toArray(selected);
	}
}
